package fun.yuxi.common.knife4j.config;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
import fun.yuxi.common.knife4j.annotation.IgnoreToken;
import fun.yuxi.common.knife4j.properties.YuXiSwaggerProperties;
import io.swagger.annotations.Api;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import springfox.documentation.RequestHandler;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.ApiSelectorBuilder;
import springfox.documentation.spring.web.plugins.Docket;

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * Knife4j 配置
 *
 * @author 剧终
 * @version V1.0
 * @date 2021年09月01日 17:47
 */
@Slf4j
@Configuration
@EnableKnife4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@ConditionalOnProperty(name = "spring.yuxi.swagger.enable", havingValue = "true", matchIfMissing = true)
public class CustomSwaggerAutoConfig {

    private final OpenApiExtensionResolver openApiExtensionResolver;
    private final RequestMappingHandlerMapping handlerMapping;
    private final YuXiSwaggerProperties swaggerProperties;
    @Value("${server.servlet.context-path:}")
    private String contextPath;

    @Bean
    public Docket createRestApi() {
        String groupName = swaggerProperties.getGroupName();
        Predicate<RequestHandler> selector = StrUtil.isNotBlank(swaggerProperties.getBasePackage())
                ? RequestHandlerSelectors.basePackage(swaggerProperties.getBasePackage())
                : RequestHandlerSelectors.withClassAnnotation(Api.class);
        ApiSelectorBuilder selectorBuilder = new Docket(DocumentationType.OAS_30)
                .host(swaggerProperties.getHost())
                .apiInfo(apiInfo())
                .groupName(groupName)
                .select()
                .apis(selector);
        // 接口扫描选择
        MDC.put("Swagger-Excludes", Arrays.toString(swaggerProperties.getExcludeUrls()));
        if (ArrayUtil.isNotEmpty(swaggerProperties.getIncludeUrls())) {
            Arrays.stream(swaggerProperties.getIgnoreAuthUrls()).map(PathSelectors::ant).forEach(selectorBuilder::paths);
        } else if (ArrayUtil.isNotEmpty(swaggerProperties.getExcludeUrls())) {
            Arrays.stream(swaggerProperties.getExcludeUrls()).forEach(x -> selectorBuilder.paths(PathSelectors.ant(x).negate()));
        } else {
            selectorBuilder.paths(PathSelectors.any());
        }
        log.info("排除路径: {}", MDC.get("Swagger-Excludes"));
        MDC.remove("Swagger-Excludes");
        //赋予插件体系
        return selectorBuilder.build().extensions(openApiExtensionResolver.buildExtensions(groupName))
                .securityContexts(securityContexts())
                .securitySchemes(securitySchemes());
    }

    private List<SecurityScheme> securitySchemes() {
        String tokenName = swaggerProperties.getTokenName();
        return CollUtil.newArrayList(new ApiKey(tokenName, tokenName, "header"));
    }

    private List<SecurityContext> securityContexts() {
        List<SecurityContext> securityContexts = new ArrayList<>();
        // 获取所有URL对应关系
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
        // 筛选出需要忽略Token的URI
        Set<String> collect = handlerMethods.keySet().parallelStream()
                .filter(x -> handlerMethods.get(x).hasMethodAnnotation(IgnoreToken.class))
                .map(RequestMappingInfo::getPatternsCondition)
                .filter(Objects::nonNull)
                .map(PatternsRequestCondition::getPatterns)
                .flatMap(Collection::stream)
                .collect(Collectors.toSet());

        AntPathMatcher antPathMatcher = new AntPathMatcher();
        String[] antMatchers = swaggerProperties.getExcludeUrls();
        List<String> pathWhitelist = Arrays.stream(antMatchers)
                .map(x -> StrUtil.join(StrUtil.EMPTY, contextPath, x))
                .collect(Collectors.toList());

        SecurityContext securityContext = SecurityContext.builder()
                .securityReferences(defaultAuth())
                .operationSelector(operationContext -> {
                    String pattern = operationContext.requestMappingPattern();
                    boolean anyMatch = pathWhitelist.parallelStream().anyMatch(x -> antPathMatcher.match(x, pattern));
                    return !(collect.contains(pattern) || anyMatch);
                }).build();
        securityContexts.add(securityContext);
        return securityContexts;
    }

    private List<SecurityReference> defaultAuth() {
        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        List<SecurityReference> securityReferences = new ArrayList<>();
        securityReferences.add(new SecurityReference(swaggerProperties.getTokenName(), authorizationScopes));
        return securityReferences;
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title(swaggerProperties.getTitle())
                .description(swaggerProperties.getDescription())
                .termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl())
                .license(swaggerProperties.getLicense())
                .licenseUrl(swaggerProperties.getLicenseUrl())
                .contact(swaggerProperties.getContact().create())
                .version(swaggerProperties.getVersion())
                .build();
    }

}
